package com.heima.article.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.article.mapper.ApHotArticlesMapper;
import com.heima.article.msg.ArticleBehaviorStreamMsg;
import com.heima.article.pojo.ApArticle;
import com.heima.article.pojo.ApHotArticles;
import com.heima.article.service.ApArticleService;
import com.heima.article.service.ApHotArticlesService;
import com.heima.common.constants.message.RedisConstants;
import com.heima.common.util.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.LocalDate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

/**
 * <p>
 * 热文章表 服务实现类
 * </p>
 *
 * @author HM
 * @since 2023-03-22
 */
@Slf4j
@Service
public class ApHotArticlesServiceImpl extends ServiceImpl<ApHotArticlesMapper, ApHotArticles> implements ApHotArticlesService {

    @Autowired
    ApArticleService articleService;

    @Autowired
    StringRedisTemplate redisTemplate;

    /**
     * 更新文章分值
     *
     * @param streamMsg
     */
    @Override
    public void updateArticleScore(ArticleBehaviorStreamMsg streamMsg) {

        ApArticle article = getApArticleByMsg(streamMsg);

        double score = this.calculateArticleScore(article);
        redisTemplate.boundZSetOps(RedisConstants.ARTICLE_SCORE_KEY)
                .incrementScore(streamMsg.getArticleId() + "", score);
        // 记录行为详细数据

        // 存两个结构：第1个的key是2023-04-02:articleId, value是行为数据的json
        // 更新具体的行为数据
        redisTemplate
                .boundValueOps(this.buildBehaviorUpdateKey(streamMsg.getArticleId(), 0))
                .set(JSON.toJSONString(this.getUpdateArticle(article)));
        // 第2个的key是固定值，比如：2023-04-02:article_score_set   value是set结构，内容是需要同步的articleId的列表
        // 存储行为数据Id的Set集合
        redisTemplate.boundSetOps(this.buildUpdateSetKey(0)).add(streamMsg.getArticleId() + "");

        // 将来定时任务同步redis数据到mysql的时候，先根据第2个key找到所有需要同步到articleId，根据第一个key，找到是需要同步的行为数据
        // xxljob起一个定时任务，把redis中的行为数据，写入到数据库
    }


    /**
     * 凌晨一点的xxl-job定时写库任务 从redis中获取用户行为信息, 写入数据库
     */
    @Override
    public void redisDataToDB() {
        String setKey = this.buildUpdateSetKey(1);
        // 从redis中获取到id集合
        Set<String> articleIdSet = redisTemplate.boundSetOps(setKey).members();
        if (CollectionUtils.isEmpty(articleIdSet)) {
            log.warn(" {},redis中没有用户行为信息数据", this.getDatePrefix(1));
            return;
        }
        List<ApArticle> articleList = getApArticlesInRedisByArticleIdSet(articleIdSet);
        if (articleList == null) {
            return;
        }

        boolean isOk = articleService.updateBatchById(articleList);
        if (isOk) {
            this.removeYesterdayArticleInRedis(articleIdSet);
        } else {
            log.error(" {},Redis用户行为数据同步到数据库写库失败", this.getDatePrefix(1));
        }


    }


    /**
     * 计算days天前的文章的分值
     *
     * @param days
     */
    @Override
    public void calculateArticleScore(long days) {
        // 从数据库中查询到文章信息
        List<ApArticle> latestArticleList = articleService.findLatestArticleByDays(days);
        if (CollectionUtils.isEmpty(latestArticleList)) {
            log.warn("查询不到{}天内的文章记录", days);
            return;
        }

        for (ApArticle article : latestArticleList) {
            // 将分值和info存储到Redis中
            redisTemplate
                    .boundZSetOps(RedisConstants.ARTICLE_SCORE_KEY)
                    .add("" + article.getId(), this.calculateArticleScore(article));
            // TODO 将来, 如果article的数据发生了改变, redis中需要同步的去修改
            redisTemplate.boundValueOps(this.buildInfoKey(article.getId())).set(JSON.toJSONString(article));
        }
        log.info("定时计算文章完成");
    }

    /**
     * 从redis中获取热点文章的id集合, 由分值从大到小排序
     *
     * @return
     */
    @Override
    public Set<String> getHotArticleIdReverseRangeByScore() {
        return redisTemplate.boundZSetOps(RedisConstants.ARTICLE_SCORE_KEY).reverseRangeByScore(0, Double.MAX_VALUE);
    }

    /**
     * 加载App首页文章 通过分值由高到低排序
     *
     * @return
     */
    @Override
    public List<ApArticle> loadApp() {
        Set<String> hotArticleIds = this.getHotArticleIdReverseRangeByScore();
        if (CollectionUtils.isEmpty(hotArticleIds)) {
            log.warn("redis中不存在热点文章id");
            return new ArrayList<>();
        }

        List<ApArticle> articleList = new ArrayList<>(hotArticleIds.size());

        // 尝试通过有序的热点id获取Redis中存储的热点信息
        for (String hotArticleId : hotArticleIds) {
            String articleJson = redisTemplate.boundValueOps(buildInfoKey(Long.valueOf(hotArticleId))).get();
            ApArticle apArticle = JsonUtils.toBean(articleJson, ApArticle.class);
            if (apArticle == null) {
                log.error("无法从Redis存储的Json中解析出apArticle对象 json: {}", articleJson);
                continue;
            }
            articleList.add(apArticle);
        }

        return articleList;
    }


    /**
     * 从redis中删除昨天的用户行为信息
     *
     * @param articleIdSet 用户id的set集合
     */
    private void removeYesterdayArticleInRedis(Set<String> articleIdSet) {
        // 清理Redis 删除Set集合
        redisTemplate.delete(this.buildUpdateSetKey(1));
        for (String articleId : articleIdSet) {
            // 删除昨日的用户数据
            redisTemplate.delete(this.buildBehaviorUpdateKey(Long.valueOf(articleId), 1));
        }
    }

    /**
     * 通过articleId的集合 获取Redis中的Ap文章
     *
     * @param articleIds
     * @return
     */
    private List<ApArticle> getApArticlesInRedisByArticleIdSet(Collection<String> articleIds) {

        List<ApArticle> articleList = new ArrayList<>(articleIds.size());
        // 根据id集合获取需要存储的数据
        for (String articleId : articleIds) {
            String articleJson = redisTemplate
                    .boundValueOps(this.buildBehaviorUpdateKey(Long.valueOf(articleId), 1)).get();
            ApArticle articleRedis = JsonUtils.toBean(articleJson, ApArticle.class);
            if (articleRedis != null) {

                ApArticle apArticleDb = articleService.getById(articleRedis.getId());
                if (apArticleDb == null) {
                    log.error("无法根据redis中的articleId查询到数据库中的article信息");
                    continue;
                }
                // 将数据库中已有的数据进行累加
                this.addArticleBehaviorData(articleRedis, apArticleDb);
                articleList.add(articleRedis);
            }
        }
        // 如果最终articleList为空, 不需要写库, 直接return, 同时记录一个警告
        if (CollectionUtils.isEmpty(articleList)) {
            log.warn(" {},无法根据redis中的用户id查询到用户行为信息数据", this.getDatePrefix(1));
            return null;
        }
        return articleList;
    }

    /**
     * 构建需要更新的行为数据
     *
     * @param article
     * @return
     */
    private ApArticle getUpdateArticle(ApArticle article) {
        // 从Redis中获取行为数据
        String articleJson = redisTemplate.boundValueOps(this.buildBehaviorUpdateKey(article.getId(), 0)).get();
        // 如果获取的数据为空,说明Redis中还没有该article, 直接返回
        if (StringUtils.isEmpty(articleJson)) {
            return article;
        }
        ApArticle articleRedis = JsonUtils.toBean(articleJson, ApArticle.class);

        return addArticleBehaviorData(article, articleRedis);

    }

    /**
     * 将target中的用户行为信息追加到source的用户行为信息中
     *
     * @param source 基础, 最终返回的对象
     * @param target 要追加的对象
     * @return
     */
    private ApArticle addArticleBehaviorData(ApArticle source, ApArticle target) {
        source.setViews(target.getViews() + source.getViews());
        source.setLikes(target.getLikes() + source.getLikes());
        source.setComments(target.getComments() + source.getComments());
        source.setCollection(target.getCollection() + source.getCollection());

        return source;
    }

    /**
     * 构建定时任务更新的Set集合的key
     *
     * @return
     */
    private String buildUpdateSetKey(int minusDays) {

        return this.getDatePrefix(minusDays) + ":" + RedisConstants.HOT_ARTICLE_UPDATE_IDS_KEY;
    }


    /**
     * 根据 ArticleBehaviorStreamMsg 构建 ApArticle对象
     *
     * @param streamMsg
     * @return
     */
    private ApArticle getApArticleByMsg(ArticleBehaviorStreamMsg streamMsg) {
        ApArticle article = new ApArticle();
        article.setId(streamMsg.getArticleId());
        article.setViews(Long.valueOf(streamMsg.getViews()));
        article.setLikes(Long.valueOf(streamMsg.getLikes()));
        article.setComments(Long.valueOf(streamMsg.getComment()));
        article.setCollection(Long.valueOf(streamMsg.getCollection()));
        return article;
    }

    /**
     * 要通过定时任务更新的行为数据的key, 由三部分构成:
     * 时间戳 + key + articleId
     *
     * @param articleId
     * @return
     */
    private String buildBehaviorUpdateKey(Long articleId, Integer minusDays) {
        return this.getDatePrefix(minusDays) + ":" + RedisConstants.HOT_ARTICLE_UPDATE_KEY + ":" + articleId;
    }

    /**
     * 获取一个前缀的时间戳 格式为 yyyy-MM-dd
     * 例: 2023-04-02
     *
     * @param minusDays 要往前延长的天数
     * @return
     */
    private String getDatePrefix(int minusDays) {
        return LocalDate.now().minusDays(minusDays).toString("yyyy-MM-dd");
    }


    /**
     * 构建 redis中 article info的key
     *
     * @param id
     * @return
     */
    private String buildInfoKey(Long id) {

        return RedisConstants.ARTICLE_INIT_PREFIX + ":" + id;
    }

    /**
     * 计算 article的分数
     *
     * @param article
     * @return
     */
    private double calculateArticleScore(ApArticle article) {
        double score = 0;
        score += article.getViews() == null ? 0 : article.getViews();
        score += article.getLikes() == null ? 0 : article.getLikes() * 3;
        score += article.getComments() == null ? 0 : article.getComments() * 5;
        score += article.getCollection() == null ? 0 : article.getCollection() * 8;

        log.info("articleId: {}, score: {}", article.getId(), score);
        return score;
    }

}
